<template>
    <div>
        <div id="info">
            <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgl - spotlight<br />
        </div>
        <div ref="rendererContainer"></div>
    </div>
</template>

<script>
import * as THREE from 'three';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { ref, onMounted, onBeforeUnmount, markRaw, shallowRef } from 'vue';

export default {
    name: 'ThreeJSComponent',
    setup() {
        const rendererContainer = shallowRef(null);
        const renderer = shallowRef(null);
        const camera = shallowRef(null);
        const spotLight = ref(null);
        const lightHelper = ref(null);
        const controls = ref(null);
        const gui = ref(null);

        const scene = markRaw(new THREE.Scene());

        const init = () => {
            renderer.value = markRaw(new THREE.WebGLRenderer({ antialias: true }));
            renderer.value.setPixelRatio(window.devicePixelRatio);
            renderer.value.setSize(window.innerWidth, window.innerHeight);
            renderer.value.setAnimationLoop(animate);
            rendererContainer.value.appendChild(renderer.value.domElement);

            renderer.value.shadowMap.enabled = true;
            renderer.value.shadowMap.type = THREE.PCFSoftShadowMap;
            renderer.value.toneMapping = THREE.ACESFilmicToneMapping;
            renderer.value.toneMappingExposure = 1;

            camera.value = markRaw(new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 100));
            camera.value.position.set(7, 4, 1);

            controls.value = markRaw(new OrbitControls(camera.value, renderer.value.domElement));
            controls.value.minDistance = 2;
            controls.value.maxDistance = 10;
            controls.value.maxPolarAngle = Math.PI / 2;
            controls.value.target.set(0, 1, 0);
            controls.value.update();

            const ambient = new THREE.HemisphereLight(0xffffff, 0x8d8d8d, 0.15);
            scene.add(ambient);

            const loader = new THREE.TextureLoader().setPath('textures/');
            const filenames = ['disturb.jpg', 'colors.png', 'uv_grid_opengl.jpg'];
            const textures = { none: null };

            for (let i = 0; i < filenames.length; i++) {
                const filename = filenames[i];
                const texture = loader.load(filename);
                texture.minFilter = THREE.LinearFilter;
                texture.magFilter = THREE.LinearFilter;
                texture.colorSpace = THREE.SRGBColorSpace;
                textures[filename] = texture;
            }

            spotLight.value = markRaw(new THREE.SpotLight(0xffffff, 100));
            spotLight.value.position.set(2.5, 5, 2.5);
            spotLight.value.angle = Math.PI / 6;
            spotLight.value.penumbra = 1;
            spotLight.value.decay = 2;
            spotLight.value.distance = 0;
            spotLight.value.map = textures['disturb.jpg'];
            spotLight.value.castShadow = true;
            spotLight.value.shadow.mapSize.width = 1024;
            spotLight.value.shadow.mapSize.height = 1024;
            spotLight.value.shadow.camera.near = 1;
            spotLight.value.shadow.camera.far = 10;
            spotLight.value.shadow.focus = 1;
            scene.add(spotLight.value);

            lightHelper.value = markRaw(new THREE.SpotLightHelper(spotLight.value));
            scene.add(lightHelper.value);

            const geometry = new THREE.PlaneGeometry(200, 200);
            const material = new THREE.MeshLambertMaterial({ color: 0xbcbcbc });
            const mesh = new THREE.Mesh(geometry, material);
            mesh.position.set(0, -1, 0);
            mesh.rotation.x = -Math.PI / 2;
            mesh.receiveShadow = true;
            scene.add(mesh);

            new PLYLoader().load('models/Lucy100k.ply', (geometry) => {
                geometry.scale(0.0024, 0.0024, 0.0024);
                geometry.computeVertexNormals();
                const material = new THREE.MeshLambertMaterial();
                const mesh = markRaw(new THREE.Mesh(geometry, material));
                mesh.rotation.y = -Math.PI / 2;
                mesh.position.y = 0.8;
                mesh.castShadow = true;
                mesh.receiveShadow = true;
                scene.add(mesh);
            });

            gui.value = markRaw(new GUI());
            const params = {
                map: textures['disturb.jpg'],
                color: spotLight.value.color.getHex(),
                intensity: spotLight.value.intensity,
                distance: spotLight.value.distance,
                angle: spotLight.value.angle,
                penumbra: spotLight.value.penumbra,
                decay: spotLight.value.decay,
                focus: spotLight.value.shadow.focus,
                shadows: true
            };

            gui.value.add(params, 'map', textures).onChange((val) => {
                spotLight.value.map = val;
            });

            gui.value.addColor(params, 'color').onChange((val) => {
                spotLight.value.color.setHex(val);
            });

            gui.value.add(params, 'intensity', 0, 500).onChange((val) => {
                spotLight.value.intensity = val;
            });

            gui.value.add(params, 'distance', 0, 20).onChange((val) => {
                spotLight.value.distance = val;
            });

            gui.value.add(params, 'angle', 0, Math.PI / 3).onChange((val) => {
                spotLight.value.angle = val;
            });

            gui.value.add(params, 'penumbra', 0, 1).onChange((val) => {
                spotLight.value.penumbra = val;
            });

            gui.value.add(params, 'decay', 1, 2).onChange((val) => {
                spotLight.value.decay = val;
            });

            gui.value.add(params, 'focus', 0, 1).onChange((val) => {
                spotLight.value.shadow.focus = val;
            });

            gui.value.add(params, 'shadows').onChange((val) => {
                renderer.value.shadowMap.enabled = val;
                scene.traverse((child) => {
                    if (child.material) {
                        child.material.needsUpdate = true;
                    }
                });
            });

            gui.value.open();
        };

        const onWindowResize = () => {
            camera.value.aspect = window.innerWidth / window.innerHeight;
            camera.value.updateProjectionMatrix();
            renderer.value.setSize(window.innerWidth, window.innerHeight);
        };

        const animate = () => {
            const time = performance.now() / 3000;
            spotLight.value.position.x = Math.cos(time) * 2.5;
            spotLight.value.position.z = Math.sin(time) * 2.5;
            lightHelper.value.update();
            renderer.value.render(scene, camera.value);
        };

        onMounted(() => {
            init();
            window.addEventListener('resize', onWindowResize);
        });

        onBeforeUnmount(() => {
            if (controls.value) {
                controls.value.dispose();
            }
            renderer.value.setAnimationLoop(null);
            if (gui.value) {
                gui.value.destroy();
            }
            window.removeEventListener('resize', onWindowResize);
        });

        return {
            rendererContainer
        };
    }
};
</script>

<style scoped>
#info {
    position: absolute;
    top: 0px;
    width: 100%;
    color: #ffffff;
    padding: 5px;
    font-family: Monospace;
    font-size: 13px;
    font-weight: bold;
    text-align: center;
    background-color: rgba(0, 0, 0, 0.5);
    z-index: 1;
}
</style>
